Modelos geográficamente ponderados#

Librerías y modulos necesarios#

import folium
import warnings
import unidecode
import numpy as np
import pandas as pd
import geopandas as gpd
import statsmodels.api as sm
warnings.filterwarnings('ignore')
from geopy.distance import geodesic
from folium.features import GeoJsonTooltip
from funciones import *

Conjunto de datos 1: Casos de defunción#

Inicialmente, se realiza la carga del conjunto de datos que contiene los registros de defunciones en Colombia, correspondientes al periodo comprendido entre 2009 y 2023. El enfoque se centra exclusivamente en mujeres mayores de 40 años, debido a su relevancia epidemiológica para el estudio de cáncer de mama.

data = pd.read_csv('../data_tasas/data_to_models.csv')
data.drop(columns=['Unnamed: 0'], inplace=True)
reemplazos = {
    'CHOCÓ': 'CHOCO',
    'CÓRDOBA': 'CORDOBA',
    'ATLÁNTICO': 'ATLANTICO',
    'BOLÍVAR': 'BOLIVAR',
    'BOYACÁ': 'BOYACA',
    'CAQUETÁ': 'CAQUETA',
    'ARCHIPIÉLAGO DE SAN ANDRÉS': 'SAN ANDRES PROVIDENCIA Y SANTA CATALINA',
}
data['Nombre_Departamento_Def'] = data['Nombre_Departamento_Def'].replace(reemplazos)

Se observan a continuación los valores únicos de variables como el año de defunción y el grupo etario.

data['año_def'].unique()
array([2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
       2020, 2021, 2022, 2023], dtype=int64)
data['grupo_edad'].unique()
array(['40-54 años', '65-74 años', '75+ años', '55-64 años'], dtype=object)

Con el fin de ajustar adecuadamente el modelo, se requiere incorporar tanto el periodo como la edad como variables explicativas. Dado que ambas variables se encuentran agrupadas en quinquenios, se optó por representar cada intervalo mediante su punto medio. Por ejemplo, el periodo 2009–2013 se representa con el año 2011. De manera análoga, los grupos etarios (también organizados en quinquenios) se codificaron utilizando la edad central de cada rango. Esta transformación permite una mejor interpretación e implementación en modelos paramétricos.

def obtener_quinquenio(anio):
    if 2009 <= anio <= 2013:
        return 2011
    elif 2014 <= anio <= 2018:
        return 2016
    elif 2019 <= anio <= 2023:
        return 2021
    else:
        return None  # o ajusta según otros casos

data['quinquenio'] = data['año_def'].apply(obtener_quinquenio)
# Función para estimar la edad central de cada grupo
def edad_media(grupo):
    if "75" in grupo or "+" in grupo:
        return 80  # asumimos edad central para grupo abierto
    else:
        partes = grupo.replace("años", "").replace("+", "").strip().split("-")
        return (int(partes[0]) + int(partes[1])) // 2

# Aplicar la función
data['edad_central'] = data['grupo_edad'].apply(edad_media)

# Mostrar un resumen
data[['año_def', 'quinquenio', 'grupo_edad', 'edad_central']].head()
año_def quinquenio grupo_edad edad_central
0 2009 2011 40-54 años 47
1 2009 2011 65-74 años 69
2 2009 2011 40-54 años 47
3 2009 2011 40-54 años 47
4 2009 2011 40-54 años 47
df_casos = data.groupby(['Nombre_Departamento_Def', 'quinquenio', 'edad_central']).size().reset_index(name='casos')
df_casos.head()
Nombre_Departamento_Def quinquenio edad_central casos
0 AMAZONAS 2011 47 1
1 AMAZONAS 2011 69 1
2 AMAZONAS 2016 47 4
3 AMAZONAS 2021 47 2
4 AMAZONAS 2021 59 1

Conjunto de datos 2: Población#

Ahora se realiza el mismo procedimiento para el conjunto de datos de la población de mujeres mayores de 40 años en el período de tiempo que se está trabajando.

df_pob = pd.read_excel('../data_tasas/POB-M40-MUNCOL-2009-2023.xlsx')
df_pob['quinquenio'] = df_pob['ANIO'].apply(obtener_quinquenio)

grupo_40_54 = [f'Mujeres_{i}' for i in range(40, 55)]
grupo_55_64 = [f'Mujeres_{i}' for i in range(55, 65)]
grupo_65_74 = [f'Mujeres_{i}' for i in range(65, 75)]
grupo_75_mas = [f'Mujeres_{i}' for i in range(75, 85)] + ['Mujeres_85 y más']

columnas_disponibles = set(df_pob.columns)
grupo_75_mas = [col for col in grupo_75_mas if col in columnas_disponibles]

df_pob['40-54'] = df_pob[grupo_40_54].sum(axis=1)
df_pob['55-64'] = df_pob[grupo_55_64].sum(axis=1)
df_pob['65-74'] = df_pob[grupo_65_74].sum(axis=1)
df_pob['75+']   = df_pob[grupo_75_mas].sum(axis=1)

df_pob_grouped = df_pob.groupby(['DPNOM', 'quinquenio'])[['40-54', '55-64', '65-74', '75+']].sum().reset_index()

df_pob_long = df_pob_grouped.melt(
    id_vars=['DPNOM', 'quinquenio'],
    var_name='grupo_edad',
    value_name='poblacion'
)

mapa_edades = {'40-54': 47, '55-64': 60, '65-74': 70, '75+': 80}
df_pob_long['edad_central'] = df_pob_long['grupo_edad'].map(mapa_edades)

df_pob_long.rename(columns={'DPNOM': 'Nombre_Departamento_Def'}, inplace=True)
reemplazos = {
    'CHOCÓ': 'CHOCO',
    'CÓRDOBA': 'CORDOBA',
    'ATLÁNTICO': 'ATLANTICO',
    'BOLÍVAR': 'BOLIVAR',
    'BOYACÁ': 'BOYACA',
    'CAQUETÁ': 'CAQUETA',
    'ARCHIPIÉLAGO DE SAN ANDRÉS': 'SAN ANDRES PROVIDENCIA Y SANTA CATALINA',
}
df_pob_long['Nombre_Departamento_Def'] = df_pob_long['Nombre_Departamento_Def'].replace(reemplazos)
df_pob_long.head()
Nombre_Departamento_Def quinquenio grupo_edad poblacion edad_central
0 AMAZONAS 2011 40-54 19415 47
1 AMAZONAS 2016 40-54 22653 47
2 AMAZONAS 2021 40-54 26892 47
3 ANTIOQUIA 2011 40-54 3050743 47
4 ANTIOQUIA 2016 40-54 3133634 47

Se completa la base de datos asignando un valor de cero en aquellos casos donde un departamento no registró defunciones durante un determinado quinquenio y grupo etario. Esto garantiza que todas las combinaciones posibles de departamento, periodo y edad estén representadas en el conjunto de datos, permitiendo un análisis consistente y sin omisiones estructurales.

departamentos = df_pob_long['Nombre_Departamento_Def'].unique()
quinquenios = df_pob_long['quinquenio'].unique()
edades = df_pob_long['edad_central'].unique()

from itertools import product
combinaciones = pd.DataFrame(
    list(product(departamentos, quinquenios, edades)),
    columns=['Nombre_Departamento_Def', 'quinquenio', 'edad_central']
)

df_casos_completo = combinaciones.merge(
    df_casos,
    on=['Nombre_Departamento_Def', 'quinquenio', 'edad_central'],
    how='left'
)

df_casos_completo['casos'] = df_casos_completo['casos'].fillna(0).astype(int)
df_casos_completo.head()
Nombre_Departamento_Def quinquenio edad_central casos
0 AMAZONAS 2011 47 1
1 AMAZONAS 2011 60 0
2 AMAZONAS 2011 70 0
3 AMAZONAS 2011 80 0
4 AMAZONAS 2016 47 4

Se realiza la unión entre el conjunto de datos de población y los registros de defunción, utilizando como claves de emparejamiento el departamento, el quinquenio y el grupo etario (representado por su edad central). Esta integración permite incorporar la población en riesgo asociada a cada combinación, lo cual es fundamental para ajustar las tasas de mortalidad y aplicar modelos estadísticos apropiados.

df_modelo = df_casos_completo.merge(df_pob_long, on=['Nombre_Departamento_Def', 'quinquenio', 'edad_central'])
df_modelo.head()
Nombre_Departamento_Def quinquenio edad_central casos grupo_edad poblacion
0 AMAZONAS 2011 47 1 40-54 19415
1 AMAZONAS 2011 60 0 55-64 6810
2 AMAZONAS 2011 70 0 65-74 3467
3 AMAZONAS 2011 80 0 75+ 2858
4 AMAZONAS 2016 47 4 40-54 22653

Posteriormente, se agrupa el conjunto de departamentos que conforman el denominado GRUPO AMAZONA, el cual incluye a Amazonas, Guainía, Guaviare, Vaupés y Vichada. Para este grupo, se realiza una agregación de los casos y de la población en cada combinación de quinquenio y grupo etario, permitiendo calcular las mismas métricas utilizadas para el resto de los departamentos. Esta consolidación responde a la baja frecuencia de eventos en estas regiones, lo cual puede generar inestabilidad en las estimaciones individuales.

departamentos_amazona = ['AMAZONAS', 'GUAINIA', 'GUAVIARE', 'VAUPES', 'VICHADA']
df_amazona = df_modelo[df_modelo['Nombre_Departamento_Def'].isin(departamentos_amazona)]
df_grupo_amazona = df_amazona.groupby(['quinquenio', 'edad_central', 'grupo_edad'], as_index=False)[['casos', 'poblacion']].sum()
df_grupo_amazona['Nombre_Departamento_Def'] = 'GRUPO AMAZONA'
df_grupo_amazona = df_grupo_amazona[
    ['Nombre_Departamento_Def', 'quinquenio', 'edad_central', 'casos', 'grupo_edad', 'poblacion']
]
df_modelo = df_modelo[~df_modelo['Nombre_Departamento_Def'].isin(departamentos_amazona)]
df_modelo = pd.concat([df_modelo, df_grupo_amazona], ignore_index=True)
reemplazos = {
    'CHOCÓ': 'CHOCO',
    'CÓRDOBA': 'CORDOBA',
    'ATLÁNTICO': 'ATLANTICO',
    'BOLÍVAR': 'BOLIVAR',
    'BOYACÁ': 'BOYACA',
    'CAQUETÁ': 'CAQUETA',
    'ARCHIPIÉLAGO DE SAN ANDRÉS Y PROVIDENCIA Y SANTA CATALINA': 'SAN ANDRES PROVIDENCIA Y SANTA CATALINA',
}
df_modelo['Nombre_Departamento_Def'] = df_modelo['Nombre_Departamento_Def'].replace(reemplazos)

Conjunto de datos 3: Coordenadas de los departamentos#

departamentos= gpd.read_file('../map_files/MGN_ANM_DPTOS.shp')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].apply(lambda x: unidecode.unidecode(x).upper())
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('BOLAVAR', 'BOLIVAR')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('NARIAO', 'NARIÑO')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('CHOCA', 'CHOCO')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('VAUPAS', 'VAUPES')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('GUAINAA', 'GUAINIA')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace('CARDOBA', 'CORDOBA')
departamentos['DPTO_CNMBR'] = departamentos['DPTO_CNMBR'].str.replace( 'ARCHIPIALAGO DE SAN ANDRAS, PROVIDENCIA Y SANTA CATALINA',
                                                                      'SAN ANDRES PROVIDENCIA Y SANTA CATALINA')
departamentos['NEW_DEPTO'] = departamentos['DPTO_CNMBR']
filtrar = ['NEW_DEPTO', 'LATITUD', 'LONGITUD', 'geometry']
departamentos = departamentos[filtrar]
df_coords = departamentos.copy()
df_coords['NEW_DEPTO'].unique()
eliminar = ['BOGOTA, D.C.']
df_coords = df_coords[~df_coords['NEW_DEPTO'].isin(eliminar)]

Ahora, vamos a calcular estas métricas para el GRUPO AMAZONA

grupo_coords = df_coords[df_coords['NEW_DEPTO'].isin(departamentos_amazona)]
grupo_coords
NEW_DEPTO LATITUD LONGITUD geometry
4 GUAINIA 2.727843 -68.816613 POLYGON ((-67.67638 3.91228, -67.67305 3.90931...
5 VICHADA 4.713557 -69.414000 POLYGON ((-67.80972 6.32432, -67.80946 6.32432...
7 AMAZONAS -1.546228 -71.502129 POLYGON ((-71.14469 0.05572, -71.14508 0.05349...
8 VAUPES 0.646246 -70.561406 POLYGON ((-70.11033 2.08010, -70.10981 2.08002...
9 GUAVIARE 1.924532 -72.128596 POLYGON ((-71.31266 2.92463, -71.31215 2.92445...
lat_grupo = grupo_coords['LATITUD'].mean()
lon_grupo = grupo_coords['LONGITUD'].mean()
geometry = grupo_coords.unary_union
df_coords = df_coords[df_coords['NEW_DEPTO'] != 'GRUPO AMAZONA']

df_coords = pd.concat([df_coords,pd.DataFrame([{'NEW_DEPTO': 'GRUPO AMAZONA', 'LATITUD': lat_grupo, 'LONGITUD': lon_grupo,'geometry':geometry}])],
                      ignore_index=True)
df_coords = df_coords[~df_coords['NEW_DEPTO'].isin(departamentos_amazona)]
df_coords = df_coords.reset_index(drop=True)

Modelamiento#

Explicación del modelo ajustado#

Con el fin de estimar la tendencia temporal de la mortalidad por cáncer de mama en Colombia, se empleó un modelo lineal generalizado (GLM) con distribución de Poisson, dado que la variable de interés corresponde al conteo de eventos (número de defunciones). Este modelo permite analizar cómo varía el número de casos según el tiempo (periodo) y la edad, ajustando por la población en riesgo como offset.

Dado que tanto el tiempo como la edad están agrupados en intervalos de cinco años (quinquenios), se utilizó el punto medio de cada uno de estos intervalos como variable explicativa continua. Así, el modelo estima la evolución temporal suavizada de las tasas de mortalidad para cada departamento.

Además, con el objetivo de incorporar la estructura espacial de los datos y mejorar la estabilidad de las estimaciones en territorios con menor número de eventos, se aplicaron modelos geográficamente ponderados. En estos modelos, para cada departamento se utilizó la información de todos los demás departamentos, pero asignando pesos decrecientes según la distancia geográfica: a menor distancia, mayor peso. Los pesos utilizados fueron de tipo gaussiano.

El modelo ajustado tiene la siguiente forma:

\[ \log(\eta) = \log(p) + \beta_0 + \beta_1 x_1 + \beta_2 x_2 \]

Donde:

  • \(\eta = E(Y)\): valor esperado del número de defunciones por cáncer de mama.

  • \(Y\): número observado de casos.

  • \(p\): población estimada en riesgo (utilizada como offset).

  • \(x_1\): año central del quinquenio (variable que representa el tiempo).

  • \(x_2\): edad central del grupo etario.

  • \(\beta_0\): intercepto del modelo.

  • \(\beta_1\): coeficiente de tendencia temporal.

  • \(\beta_2\): coeficiente asociado a la edad.

El coeficiente \(\beta_1\) permite evaluar la dirección y magnitud del cambio en las tasas de mortalidad a lo largo del tiempo. A partir de este coeficiente, se calcula el Cambio Promedio Porcentual Anual (CPPA) de la tasa, mediante la siguiente transformación:

\[ \text{CPPA} = 100 \cdot \left(e^{\beta_1} - 1\right) \]

Un CPPA positivo indica que, en promedio, la tasa de mortalidad ha aumentado anualmente, mientras que un CPPA negativo refleja una disminución sistemática. Este indicador es particularmente útil para evaluar tendencias en regiones con variación interanual y diferente magnitud poblacional.

Cálculo de la matriz de distancias geográficas#

En primer lugar, se extraen los nombres de los departamentos a partir del DataFrame df_coords, el cual contiene las coordenadas geográficas (latitud y longitud) asociadas a cada unidad territorial. Posteriormente, se inicializa una matriz cuadrada de ceros denominada dist_matrix, con dimensiones iguales al número total de departamentos. Esta matriz será utilizada para almacenar las distancias geográficas (en kilómetros) entre todos los pares de departamentos, y sus filas y columnas están etiquetadas con los nombres de los mismos para facilitar su interpretación y manipulación posterior.

Para el llenado de esta matriz, se implementa un doble bucle for que recorre todas las combinaciones posibles de departamentos. En cada iteración, se extraen las coordenadas geográficas (latitud y longitud) de los dos departamentos correspondientes y se calcula la distancia geodésica entre ellos utilizando la función geodesic() del paquete geopy. Esta función considera la curvatura terrestre, por lo que proporciona una estimación precisa de la distancia real entre dos puntos sobre la superficie del planeta. El resultado de cada cálculo se asigna a la celda correspondiente de la matriz de distancias.

departamentos = df_coords['NEW_DEPTO'].values
n = len(departamentos)
dist_matrix = pd.DataFrame(np.zeros((n, n)), index=departamentos, columns=departamentos)

# Calcular distancia geodésica (en km)
for i in range(n):
    for j in range(n):
        coord_i = (df_coords.loc[i, 'LATITUD'], df_coords.loc[i, 'LONGITUD'])
        coord_j = (df_coords.loc[j, 'LATITUD'], df_coords.loc[j, 'LONGITUD'])
        dist_matrix.iloc[i, j] = geodesic(coord_i, coord_j).kilometers
dist_matrix.iloc[:5, :5]
CAQUETA CAUCA PUTUMAYO VALLE DEL CAUCA CASANARE
CAQUETA 0.000000 364.483529 214.543185 442.180486 572.708079
CAUCA 364.483529 0.000000 240.510468 165.195781 668.518443
PUTUMAYO 214.543185 240.510468 0.000000 383.834120 723.433975
VALLE DEL CAUCA 442.180486 165.195781 383.834120 0.000000 571.677599
CASANARE 572.708079 668.518443 723.433975 571.677599 0.000000

Una vez calculada la matriz de distancias, se define el parámetro de suavizamiento \(h\), también conocido como bandwidth. En este caso, se selecciona un valor de referencia de 200 kilómetros, el cual establece el radio dentro del cual los departamentos vecinos tendrán un peso significativo en los modelos de regresión. Este parámetro controla la velocidad con la que decae la influencia espacial de una unidad geográfica sobre otra: a menor valor de \(h\), menor será la influencia de los departamentos alejados, mientras que un valor más alto permite que zonas más lejanas también influyan en la estimación.

Construcción de la matriz de pesos gaussianos#

Con la matriz de distancias previamente calculada y el valor de \(h\) definido, se procede a construir la matriz de pesos gaussianos weights_matrix. Este paso se realiza aplicando la función de núcleo gaussiano a cada elemento de la matriz de distancias, según la fórmula:

\[ w_{ij} = \exp\left(-\frac{d_{ij}^2}{2h^2}\right) \]

donde \(d_{ij}\) representa la distancia entre los departamentos \(i\) y \(j\), y \(w_{ij}\) es el peso asignado entre ellos. Esta función asegura que los departamentos más cercanos reciban un peso mayor en la estimación, mientras que los más lejanos tengan un peso decreciente, tendiente a cero. Como resultado, se obtiene una matriz simétrica, positiva y continua, que representa adecuadamente la dependencia espacial entre unidades territoriales en el contexto de modelos geográficamente ponderados.

# Parámetro de suavizamiento (h): elige un valor razonable, ej. 200 km
h = 200  
# Matriz de pesos gaussianos
weights_matrix = np.exp(- (dist_matrix**2) / (2 * h**2))
weights_matrix.iloc[:5, :5]
CAQUETA CAUCA PUTUMAYO VALLE DEL CAUCA CASANARE
CAQUETA 1.000000 0.190024 0.562502 0.086809 0.016574
CAUCA 0.190024 1.000000 0.485262 0.710973 0.003748
PUTUMAYO 0.562502 0.485262 1.000000 0.158562 0.001442
VALLE DEL CAUCA 0.086809 0.710973 0.158562 1.000000 0.016820
CASANARE 0.016574 0.003748 0.001442 0.016820 1.000000

Ahora se implementa un ajuste por departamento de modelos lineales generalizados (GLM) con distribución de Poisson y función de enlace logarítmica, incorporando ponderaciones espaciales mediante una matriz de pesos gaussianos. Inicialmente, se filtran los registros del conjunto de datos para asegurar la coherencia entre los nombres de los departamentos y los presentes en la matriz de pesos, y se extrae la lista de departamentos únicos a modelar. Para cada uno, se ajusta un modelo en el que se asigna a cada observación un peso \(w_{ij}\) basado en su distancia geográfica al departamento actual, lo que permite incorporar información de los demás territorios, dándole mayor importancia a los más cercanos. Se eliminan las observaciones con peso nulo y se definen las variables del modelo: los casos como variable dependiente, el quinquenio y la edad central como predictoras, y el logaritmo de la población como offset, lo que permite modelar tasas en lugar de conteos absolutos. Con los datos preparados, se ajusta el GLM y se extrae el coeficiente \(\beta_1\), asociado al tiempo, que representa el crecimiento logarítmico anual de la tasa de mortalidad. A partir de este valor se calcula el Cambio Promedio Porcentual Anual (CPPA) mediante la fórmula \(\text{CPPA} = 100 \times (e^{\beta_1} - 1)\), lo que permite interpretar tendencias temporales de la mortalidad: valores positivos indican aumento, y negativos, disminución.

resultados = []

# Asegurar que todos los nombres estén estandarizados
nombres_validos = set(weights_matrix.index)
df_modelo = df_modelo[df_modelo['Nombre_Departamento_Def'].isin(nombres_validos)]
departamentos = df_modelo['Nombre_Departamento_Def'].unique()

for depto in departamentos:
    try:
        df_temp = df_modelo.copy()

        # 2. Obtener pesos para el departamento actual como dict plano
        pesos_dict = weights_matrix.loc[depto, :].to_dict()

        # 3. Aplicar los pesos (esto es específico para el depto actual)
        df_temp['peso'] = df_temp['Nombre_Departamento_Def'].map(pesos_dict)

        # 4. Filtrar solo observaciones con peso > 0
        df_temp = df_temp[df_temp['peso'] > 0]

        # Verificar que haya datos suficientes
        if df_temp.empty or df_temp['peso'].sum() == 0:
            print(f"Departamento {depto}: sin pesos válidos, se omite.")
            continue

        # 5. Preparar las variables del modelo
        y = df_temp['casos']
        X = df_temp[['quinquenio', 'edad_central']]
        X = sm.add_constant(X)
        offset = np.log(df_temp['poblacion'])
        pesos = df_temp['peso']

        # Validar datos
        if y.isna().any() or X.isna().any().any() or np.isinf(offset).any():
            print(f"Datos inválidos para {depto}, se omite.")
            continue

        # 6. Ajustar modelo GLM Poisson con offset y pesos
        modelo = sm.GLM(y, X, family=sm.families.Poisson(), offset=offset, freq_weights=pesos)
        resultado = modelo.fit()

        # 7. Extraer coeficientes y CPPA
        beta_1 = resultado.params['quinquenio']
        cppa = 100 * (np.exp(beta_1) - 1)
        p_valor = resultado.pvalues['quinquenio']

        # 8. Guardar resultados
        resultados.append({
            'departamento': depto,
            'beta_1': beta_1,
            'CPPA': cppa,
            'p_valor': p_valor
        })

    except Exception as e:
        print(f"Error en {depto}: {str(e)}")
        continue
df_resultados = pd.DataFrame(resultados)
df_resultados.sort_values(by='CPPA', ascending=False)
df_resultados.head()
departamento beta_1 CPPA p_valor
0 ANTIOQUIA -0.011208 -1.114561 0.000159
1 ARAUCA 0.003720 0.372730 0.573211
2 SAN ANDRES PROVIDENCIA Y SANTA CATALINA -0.015796 -1.567236 0.765197
3 ATLANTICO 0.008062 0.809453 0.053729
4 BOLIVAR 0.001754 0.175564 0.595148
df_periodos = df_modelo.groupby(['Nombre_Departamento_Def', 'quinquenio'])['casos'].sum().reset_index()
df_periodos['es_inestable'] = df_periodos['casos'] < 15
df_inestabilidad = df_periodos.groupby('Nombre_Departamento_Def')['es_inestable'].mean().reset_index()
df_inestabilidad['porcentaje_inestabilidad'] = df_inestabilidad['es_inestable'] * 100
df_inestabilidad = df_inestabilidad.drop(columns='es_inestable')
df_inestabilidad.head()
Nombre_Departamento_Def porcentaje_inestabilidad
0 ANTIOQUIA 0.0
1 ARAUCA 0.0
2 ATLANTICO 0.0
3 BOLIVAR 0.0
4 BOYACA 0.0

Conjunto de datos 4: Tasas#

Se realiza el cargue del conjunto de datos que contiene las tasas de mortalidad previamente calculadas por departamento, con el propósito de unirlas con los resultados del modelo GLM. Esta unión permite consolidar en un solo conjunto de datos tanto las métricas de tendencia (como el CPPA y el valor-p) como las tasas observadas (cruda, ajustada y REM), facilitando su análisis comparativo y su posterior visualización.

df_tasas = pd.read_excel('tasas_final.xlsx')
df_tasas.head()
DPNOM FALLECIDAS TOTAL_MUJERES Tasa_Mortalidad_Cruda TAE MUERTES_ESPERADAS SMR
0 ANTIOQUIA 6185 19011651 32.532682 38.466253 7108.466479 87.008921
1 ARAUCA 137 543874 25.189658 30.488575 203.354780 67.369943
2 ARCHIPIÉLAGO DE SAN ANDRÉS 30 189144 15.860931 26.424814 70.721043 42.420189
3 ATLÁNTICO 3122 6773319 46.092617 54.323739 2532.547598 123.275077
4 BOLÍVAR 1588 5135499 30.922019 35.703283 1920.165823 82.701191
reemplazos = {
    'CHOCÓ': 'CHOCO',
    'CÓRDOBA': 'CORDOBA',
    'ATLÁNTICO': 'ATLANTICO',
    'BOLÍVAR': 'BOLIVAR',
    'BOYACÁ': 'BOYACA',
    'CAQUETÁ': 'CAQUETA',
    'ARCHIPIÉLAGO DE SAN ANDRÉS': 'SAN ANDRES PROVIDENCIA Y SANTA CATALINA',
}
df_tasas['DPNOM'] = df_tasas['DPNOM'].replace(reemplazos)

Conjunto de datos final#

df_final = df_resultados.merge(df_inestabilidad, left_on='departamento', right_on='Nombre_Departamento_Def', how='left')
df_final_final = df_final.merge(df_tasas, left_on='departamento', right_on='DPNOM', how='left')
df_final_final = df_final_final.drop(columns=['DPNOM', 'departamento'])
columnas_orden = [
    'Nombre_Departamento_Def',        # Departamento
    'TOTAL_MUJERES',                  # Población
    'FALLECIDAS',                     # Total muertes
    'Tasa_Mortalidad_Cruda',          # TC anual
    'TAE',                            # TAE anual
    'MUERTES_ESPERADAS',              # Muertes esperadas
    'SMR',                            # REM
    'CPPA',                           # % exp
    'porcentaje_inestabilidad'        # IIT (%)
]

df_final_ordenado = df_final_final[columnas_orden]
df_final_ordenado
Nombre_Departamento_Def TOTAL_MUJERES FALLECIDAS Tasa_Mortalidad_Cruda TAE MUERTES_ESPERADAS SMR CPPA porcentaje_inestabilidad
0 ANTIOQUIA 19011651 6185 32.532682 38.466253 7108.466479 87.008921 -1.114561 0.000000
1 ARAUCA 543874 137 25.189658 30.488575 203.354780 67.369943 0.372730 0.000000
2 SAN ANDRES PROVIDENCIA Y SANTA CATALINA 189144 30 15.860931 26.424814 70.721043 42.420189 -1.567236 100.000000
3 ATLANTICO 6773319 3122 46.092617 54.323739 2532.547598 123.275077 0.809453 0.000000
4 BOLIVAR 5135499 1588 30.922019 35.703283 1920.165823 82.701191 0.175564 0.000000
5 BOYACA 3751470 748 19.938851 21.533421 1402.676640 53.326617 -0.776481 0.000000
6 CALDAS 3393571 1012 29.821094 33.117195 1268.858012 79.756757 -1.271397 0.000000
7 CAQUETA 852787 203 23.804303 28.849114 318.857516 63.664800 -0.479281 0.000000
8 CASANARE 862417 159 18.436557 23.606235 322.458178 49.308720 -0.305555 0.000000
9 CAUCA 3713294 631 16.992999 19.036951 1388.402613 45.447912 -0.117864 0.000000
10 CESAR 2578697 743 28.813001 35.733376 964.176188 77.060605 0.857090 0.000000
11 CHOCO 1033862 51 4.932960 5.629636 386.561555 13.193242 -1.272380 100.000000
12 CUNDINAMARCA 7787730 9471 121.614386 141.167201 2911.836413 325.258657 -1.186650 0.000000
13 CORDOBA 4453938 1021 22.923534 26.272880 1665.329801 61.309177 -0.464055 0.000000
14 HUILA 2722786 954 35.037642 40.751545 1018.051142 93.708455 -0.508264 0.000000
15 LA GUAJIRA 1614157 247 15.302105 18.957726 603.534166 40.925604 1.448043 0.000000
16 MAGDALENA 3108562 884 28.437586 33.583236 1162.292995 76.056554 0.855081 0.000000
17 META 2451468 697 28.431944 35.316892 916.605197 76.041463 -0.917465 0.000000
18 NARIÑO 4523184 879 19.433213 21.731691 1691.220917 51.974286 0.486497 0.000000
19 NORTE DE SANTANDER 3949801 1282 32.457331 37.325233 1476.832707 86.807395 0.461413 0.000000
20 PUTUMAYO 711318 87 12.230817 15.561311 265.962181 32.711418 0.205900 33.333333
21 QUINDIO 1856017 624 33.620382 37.278988 693.965749 89.917982 -1.078770 0.000000
22 RISARALDA 3083911 1260 40.857210 45.914002 1153.075973 109.272939 -1.208586 0.000000
23 SANTANDER 6428298 2209 34.363684 38.530616 2403.544061 91.905950 -0.551912 0.000000
24 SUCRE 2245948 529 23.553528 26.969832 839.761159 62.994102 0.160764 0.000000
25 TOLIMA 4086695 1165 28.507143 31.067971 1528.017447 76.242585 -1.032853 0.000000
26 VALLE DEL CAUCA 13901078 5653 40.665911 45.505931 5197.620501 108.761307 -0.720530 0.000000
27 GRUPO AMAZONA 521805 39 7.474056 8.184462 195.103169 19.989424 -0.127677 100.000000
df_final_ordenado.to_csv('resultados_finales.csv', index=False)

Mapas CPPA#

df_final_ordenado.head()
Nombre_Departamento_Def TOTAL_MUJERES FALLECIDAS Tasa_Mortalidad_Cruda TAE MUERTES_ESPERADAS SMR CPPA porcentaje_inestabilidad
0 ANTIOQUIA 19011651 6185 32.532682 38.466253 7108.466479 87.008921 -1.114561 0.0
1 ARAUCA 543874 137 25.189658 30.488575 203.354780 67.369943 0.372730 0.0
2 SAN ANDRES PROVIDENCIA Y SANTA CATALINA 189144 30 15.860931 26.424814 70.721043 42.420189 -1.567236 100.0
3 ATLANTICO 6773319 3122 46.092617 54.323739 2532.547598 123.275077 0.809453 0.0
4 BOLIVAR 5135499 1588 30.922019 35.703283 1920.165823 82.701191 0.175564 0.0
df_coords.head()
NEW_DEPTO LATITUD LONGITUD geometry
0 CAQUETA 0.798556 -73.959468 POLYGON ((-74.89423 2.95852, -74.89410 2.95852...
1 CAUCA 2.396834 -76.824233 POLYGON ((-76.45922 3.32872, -76.45878 3.32870...
2 PUTUMAYO 0.452260 -75.855912 POLYGON ((-76.67050 1.46732, -76.67000 1.46730...
3 VALLE DEL CAUCA 3.858858 -76.518694 MULTIPOLYGON (((-77.23810 4.04049, -77.23774 4...
4 CASANARE 5.404064 -71.601881 POLYGON ((-72.33885 6.34471, -72.33920 6.34454...
df_mapa = df_final_ordenado.merge(df_coords, left_on='Nombre_Departamento_Def', right_on='NEW_DEPTO', how='left')
def get_color_cppa(cppa):
    if pd.isna(cppa):
        return "#CCCCCC"  
    if cppa < -3:
        return "#590D25"   
    elif -3 <= cppa < -1:
        return "#590D22"   
    elif -1 <= cppa < -0.5:
        return "#FF4D6D"   
    elif -0.5 <= cppa < 0.5:
        return "#E37186" 
    elif -0.3 <= cppa <0.3:
        return "#F5F5F5"
    elif 0.3 <= cppa < 0.5:
        return "#FFB3C6"
    elif 0.5 <= cppa < 1:
        return "#FF8FA3" 
    elif 1 <= cppa < 3:
        return "#D03753" 
    else:  # cppa >= 3
        return "#C9184A"  
gdf_mapa = gpd.GeoDataFrame(df_mapa, geometry='geometry', crs="EPSG:4326")
m = folium.Map(location=[4.5, -74], zoom_start=5, tiles="CartoDB positron")

folium.GeoJson(
    gdf_mapa,
    style_function=lambda feature: {
        "fillColor": get_color_cppa(feature["properties"].get("CPPA")),
        "color": "black",
        "weight": 0.5,
        "fillOpacity": 0.85,
    },
    tooltip=GeoJsonTooltip(
        fields=['Nombre_Departamento_Def', "CPPA"],
        aliases=["Departamento:", "CPPA (%)"],
        localize=True,
        sticky=True
    )
).add_to(m)

m.save(f"../map_outputs/MAPA_CPPA.html")

image.png

m
Make this Notebook Trusted to load map: File -> Trust Notebook